package generator.gridgain;

import java.io.File;

import generator.utils.java2xml.DocumentModifier;
import generator.utils.TransformDocs;

import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**Esta clase es la encargada de la generación de la clase java que representa un trabajo o tarea que será
 * enviada al grid de GridGain para su ejecución. Genéricamente dicha clase se denomina GridExecutorJob y 
 * GEJ será el sufijo del nombre de la clase generada, a través del método build(), para un trabajo en particular*/
public class GridExecutorJobBuilder extends DocumentModifier implements Buildable{
	
	/**Representa la ruta del archivo template que se utilizará para construir el documento que representa
	 * la clase GEJ*/
	private final String pathExecutoJobTemplate = new String ("conf//gridgainGen//GridExecutorJob.xml");
	
	/**Representa el método que se pretende gridificar del cual se extrae información necesaria el GEJ.
	 * */
	private Element methodToGridify = null;
		
	/**Documento que representa la clase a gridificar, del cual se extrae información como el paquete
	 * al que pertenece la clase y el nombre de la misma, necesarios para la construcción de la clase GEJ.
	 * Dicho documento es el que representa la clase del usuario que contiene las convenciones JGrim*/
	private Document classToGridify = null;
	
	/**Constructor vacío de un GridExecutorJobBuilder.
	 */
	public GridExecutorJobBuilder(){				
	}
	
	/**@param methodToGridify
	 * 			Element que representa el método que será gridificado*/
	public void setMethodToGridify(Element methodToGridify) {
		this.methodToGridify = methodToGridify;
	}

	/**@param classToGridify
	 * 			Document que representa la clase sobre la cual se encuentra el método a grifidicar*/
	public void setClassToGridify(Document classToGridify) {
		this.classToGridify = classToGridify;
	}

	/**Constructor de la clase GridExecutorJobBuilder
	 * @param classToGridify
	 * 			Documento que representa la clase que contiene el método a gridificar.
	 * @param methodToGridify
	 * 			Elemento que representa el método que se pretende gridificar.
	 */
	public GridExecutorJobBuilder(Document classToGridify, Element methodToGridify){
		this.classToGridify = classToGridify;
		this.methodToGridify = methodToGridify;
	}
	
	/** 
	 * Método encargado de generar la clase GridExecutorJob que representa un trabajo particular que será enviado al grid
	 * de GridGain.
	 * <BR>La construcción de dicha clase se realiza teniendo en cuenta los atributos {@link methodToGridify} y {@link classToGridify}.
	 * Las acciones llevadas a cabo son:
	 * <BR>
	 * 1) Agregar el package donde se va a incluir el archico GridExecutorJob y los import necesarios.
	 * 2) Modifica el nombre de la clase GEJ y las referencias al GER.
	 * 3) Modificar el tipo de la variable de clase segun el tipo de objeto que posee el método a gridificar
	 * 4) Considerando cada uno de los parametros del método a gridificar se agregan las variables de instancia,
	 *    se incorporan los parametros del método constructor, se realizar la asignacion de los parametros del constructor 
	 *    a las variables de clase y se agregan los argumentos para el llamado del método a gridificar dentro del método call
	 * 5) Se modifica el llamado del método de acuerdo a el tipo que devuelve y el nombre del método gridificado
	 */
	
	public Document build() {
		Document ejDoc = TransformDocs.xmlToDom(new File(pathExecutoJobTemplate));
		
		System.out.println("*********** Generando clase GridExecutorJob *************");
		if (methodToGridify != null && classToGridify != null){
			try {				
			// Modificación 1)				
				System.out.print("[INFO] seteo del paquete al que pertenece... ");		
				this.setPackage(classToGridify,ejDoc);
				System.out.println();
				System.out.println("[INFO] seteo de imports necesarios" );
				this.cloneImports(this.classToGridify, ejDoc);
			// Modificación 2)
				System.out.print("[INFO] seteo del nombre de clase ");
	            String prefix = (String)xpath.evaluate("//"+CLASS+"/@name", classToGridify, XPathConstants.STRING);
	            prefix += "_"+(String)xpath.evaluate("@name", methodToGridify, XPathConstants.STRING);
	            this.setClassName(prefix+"GEJ", ejDoc);
	            System.out.println(prefix+"GEJ");
	            Element methodCall = (Element) xpath.evaluate("/"+CLASSFILENAME+"/"+CLASS+"/"+METHOD+"[@name=\"call\"]/"+TYPE,ejDoc,XPathConstants.NODE);
	            methodCall.setAttribute("name", prefix+"GER");
	            Element newGER = (Element) xpath.evaluate("/"+CLASSFILENAME+"/"+CLASS+"/"+METHOD+"[@name=\"call\"]/"+BODYMETHOD+"/"+RETURN
	    				+"/"+NEW+"/"+TYPE,ejDoc,XPathConstants.NODE);
	            newGER.setAttribute("name", prefix+"GER");
	
			// Modificación 3)
				System.out.print("[INFO] seteo del tipo del Job ");
				String typeValue = (String)xpath.evaluate("/"+CLASSFILENAME+"/"+CLASS+"/"+"@name"
						,classToGridify,XPathConstants.STRING)+"Peer";
				this.setType(typeValue,ejDoc,"/"+CLASSFILENAME+"/"+CLASS+"/"+ATTRIBUTE+"[@name=\"job\"]/"+TYPE+"[1]");
				System.out.println();
			// Modificación 4)	
				Node firstParam = this.createParam(ejDoc,"job",typeValue);
				Element paramsExJob = (Element)xpath.evaluate("//"+CONSTRUCTOR+"/"+ALLPARAMS,ejDoc,XPathConstants.NODE);
				paramsExJob.appendChild(firstParam);
				System.out.print("[INFO] seteo de variables de instancia, parametros del constructor y argumentos del llamado a gridificar\n");
				NodeList allParams = (NodeList)xpath.evaluate(ALLPARAMS+"/"+PARAM,methodToGridify,XPathConstants.NODESET);
				for (int i = 0; i < allParams.getLength(); i++) {
					Element param = (Element)allParams.item(i);
					this.generateInstanceVar(ejDoc,param);
					this.addCreatorNewParam(ejDoc,param);
					this.addCreatorAssigment(ejDoc,param);
					this.addCallParams(ejDoc,param);
				}	
			// Modificación 5)
				System.out.println("[INFO] seteo de Cast a realizar dentro del método call");
				Element castType = (Element)xpath.evaluate("//"+CASTEXPR+"/"+TYPE,ejDoc,XPathConstants.NODE);			
				Element returnType = (Element) methodToGridify.getElementsByTagName(TYPE).item(FIRST);
				this.copyAttributes(returnType, castType, false);
				System.out.println("[INFO] seteo del nombre del método gridificado dentro del método call");
				Element method = (Element)xpath.evaluate("//"+METHODCALL, ejDoc,XPathConstants.NODE);
				String methodName = (String)xpath.evaluate("@name", methodToGridify, XPathConstants.STRING);
				method.setAttribute("message", methodName);
	
				
			} catch (Exception e) {
				System.out.println("[ERR] error al generar clase GridExecutorJob");
				e.printStackTrace();
			}
		}
		else
			System.err.println("[ERR] falló creación de GridExecutorJob: clase o método a gridificar no seteados");
		return ejDoc;
	}
	
	
	/** Agrega un parámetro para la llamada recursiva del método call en base al valor de {@link param}
	 * 
	 * @param ejDoc Documento que representa el GEJ.
	 * @param param Parametro del que se obtienen los datos para el nuevo parametro.
	 * @throws XPathExpressionException
	 */
	private void addCallParams(Document ejDoc, Element param) throws XPathExpressionException {
		Element argumentos = (Element)xpath.evaluate("/"+CLASSFILENAME+"/"+CLASS+"/"+METHOD+"[@name=\"call\"]/"+BODYMETHOD+"/"+RETURN
				+"/"+NEW+"/"+ARGUMENTS+"/"+CASTEXPR+"/"+METHODCALL+"/"+ARGUMENTS,ejDoc,XPathConstants.NODE);
		argumentos.appendChild(this.createNewTag(ejDoc,VARREF,param.getAttribute("name")));	
	}

	/** Crea la asignación entre uno de los parametros del constructor del GEJ y una variable de instancia
	 * 
	 * @param ejDoc Documento que representa el GEJ.
	 * @param param Parametro del que se obtienen los datos para la nueva asignación.
	 * @throws XPathExpressionException
	 */
	private void addCreatorAssigment(Document ejDoc, Element param) throws XPathExpressionException {
		Element creatorBody = (Element)xpath.evaluate("/"+CLASSFILENAME+"/"+CLASS+"/"+CONSTRUCTOR+"/"+BODYMETHOD
				,ejDoc,XPathConstants.NODE);
		Element nodeToAdd = this.setAssingmentValues(ejDoc,param.getAttribute("name"),param.getAttribute("name"));
		creatorBody.appendChild(nodeToAdd);		
	}
	
	/** Crea un nuevo parametro en el constructor del GEJ representado por {@link ejDoc},
	 *  basandose en {@link param}.<br>
	 * 	Ejemplo de {@link param} esperado: <br>
	 * 	&lt field name="varName" visibility="private"&gt <br>
     *  &lt type name="varType" /&gt <br>
     *  &lt/field&gt
	 * 
	 * @param ejDoc Documento que representa el GEJ.
	 * @param param Parametro del que se obtienen los datos para el nuevo parametro.
	 * @throws XPathExpressionException
	 */
	private void addCreatorNewParam(Document ejDoc, Element param) throws XPathExpressionException {
		Element paramsExJob = (Element)xpath.evaluate("/"+CLASSFILENAME+"/"+CLASS+"/"+CONSTRUCTOR+"/"+ALLPARAMS,ejDoc,XPathConstants.NODE);
		Node newParam = ejDoc.importNode(param, true);
		paramsExJob.appendChild(newParam);		
	}

	/** Método encargado de generar una nueva variables de instancia con los valores de {@link param}
	 * 	y la asocia a {@link ejDoc} y la inserta a continuación de la declaración de la clase.
	 *  Los valores se obtienen de {@link param} que representa un parametro de una función.
	 * 	Ejemplo de {@link param} esperado: <br>
	 * 	&lt field name="varName" visibility="private"&gt <br>
     *  &lt type name="varType" /&gt <br>
     *  &lt/field&gt
	 * 
	 * @param ejDoc Documento que representa el GEJ.
	 * @param param Parametro del que se obtienen los datos para la nueva variable.
	 * @throws XPathExpressionException
	 */
	private void generateInstanceVar(Document ejDoc, Element param) throws XPathExpressionException {
		String varName = param.getAttribute("name");
		Element varType = (Element) xpath.evaluate(TYPE, param, XPathConstants.NODE);
		Element varEj = this.createInstanceVar(ejDoc,varName, varType);
		Element clase = (Element)xpath.evaluate("/"+CLASSFILENAME+"/"+CLASS+"/"+ATTRIBUTE,ejDoc,XPathConstants.NODE);
		clase.getParentNode().insertBefore(varEj, clase);
	}
	
	/**
	 * Crea un Element que representa una variable de instancia del tipo {@link typ} 
	 * y con el nombre {@link name}. Asocia el nuevo Element a {@link doc}
	 * 
	 * @param doc Document al que se asocia el nuevo Element
	 * @param name Nombre de la variable
	 * @param type Tipo de la variable
	 * @return El Element creado
	 */	
	private Element createInstanceVar(Document doc,String name, Element typ){
		Element field = this.createNewTag(doc,ATTRIBUTE,name);
		Element type = this.createNewTag(doc,TYPE,typ.getAttribute("name"));
		this.copyAttributes(typ, type, false);
		field.setAttribute("visibility", "private");
		field.appendChild(type);
		return field;
	}
	
	/**
	 * Crea un parametro de método con el nombre {@link name} y el tipo {@link typ} 
	 * pasados como parametros asociandolo al documento {@link doc}.
	 * 
	 * @param doc Documento al que se asociara el nuevo nodo
	 * @param name nombre del nuevo parametro
	 * @param typ tipo del nuevo parametro
	 * @return El nuevo nodo parametro
	 * @throws XPathExpressionException 
	 */
	private Node createParam (Document doc,String name,String typ) throws XPathExpressionException{
		Element param = this.createNewTag(doc, PARAM, name);
		Element type = this.createNewTag(doc, TYPE, typ);
		param.appendChild(type);		
		return param;		
	}
	


}
